// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Transactions.DtcProxyShim;
using System.Transactions.DtcProxyShim.DtcInterfaces;
using System.Transactions.Oletx;

namespace System.Transactions
{
    public static class TransactionInterop
    {
        internal static OletxTransaction ConvertToOletxTransaction(Transaction transaction)
        {
            ArgumentNullException.ThrowIfNull(transaction);

            ObjectDisposedException.ThrowIf(transaction.Disposed, transaction);

            if (transaction._complete)
            {
                throw TransactionException.CreateTransactionCompletedException(transaction.DistributedTxId);
            }

            OletxTransaction? oletxTx = transaction.Promote();
            Debug.Assert(oletxTx != null, "transaction.Promote returned null instead of throwing.");

            return oletxTx;
        }

        /// <summary>
        /// This is the PromoterType value that indicates that the transaction is promoting to MSDTC.
        ///
        /// If using the variation of Transaction.EnlistPromotableSinglePhase that takes a PromoterType and the
        /// ITransactionPromoter being used promotes to MSDTC, then this is the value that should be
        /// specified for the PromoterType parameter to EnlistPromotableSinglePhase.
        ///
        /// If using the variation of Transaction.EnlistPromotableSinglePhase that assumes promotion to MSDTC and
        /// it that returns false, the caller can compare this value with Transaction.PromoterType to
        /// verify that the transaction promoted, or will promote, to MSDTC. If the Transaction.PromoterType
        /// matches this value, then the caller can continue with its enlistment with MSDTC. But if it
        /// does not match, the caller will not be able to enlist with MSDTC.
        /// </summary>
        public static readonly Guid PromoterTypeDtc = new Guid("14229753-FFE1-428D-82B7-DF73045CB8DA");

        public static byte[] GetExportCookie(Transaction transaction, byte[] whereabouts)
        {
            ArgumentNullException.ThrowIfNull(transaction);
            ArgumentNullException.ThrowIfNull(whereabouts);

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetExportCookie)}");
            }

            byte[] cookie;

            // Copy the whereabouts so that it cannot be modified later.
            var whereaboutsCopy = new byte[whereabouts.Length];
            Buffer.BlockCopy(whereabouts, 0, whereaboutsCopy, 0, whereabouts.Length);

            // First, make sure we are working with an OletxTransaction.
            OletxTransaction oletxTx = ConvertToOletxTransaction(transaction);

            try
            {
                oletxTx.RealOletxTransaction.TransactionShim.Export(whereabouts, out cookie);
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);

                // We are unsure of what the exception may mean.  It is possible that
                // we could get E_FAIL when trying to contact a transaction manager that is
                // being blocked by a fire wall.  On the other hand we may get a COMException
                // based on bad data.  The more common situation is that the data is fine
                // (since it is generated by Microsoft code) and the problem is with
                // communication.  So in this case we default for unknown exceptions to
                // assume that the problem is with communication.
                throw TransactionManagerCommunicationException.Create(null, comException);
            }

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetExportCookie)}");
            }

            return cookie;
        }

        public static Transaction GetTransactionFromExportCookie(byte[] cookie)
        {
            ArgumentNullException.ThrowIfNull(cookie);

            if (cookie.Length < 32)
            {
                throw new ArgumentException(SR.InvalidArgument, nameof(cookie));
            }

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromExportCookie)}");
            }

            var cookieCopy = new byte[cookie.Length];
            Buffer.BlockCopy(cookie, 0, cookieCopy, 0, cookie.Length);
            cookie = cookieCopy;

            Transaction? transaction;
            TransactionShim? transactionShim = null;
            Guid txIdentifier = Guid.Empty;
            OletxTransactionIsolationLevel oletxIsoLevel = OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE;
            OutcomeEnlistment? outcomeEnlistment;
            OletxTransaction? oleTx;

            // Extract the transaction guid from the propagation token to see if we already have a
            // transaction object for the transaction.
            // In a cookie, the transaction guid is preceded by a signature guid.
            var txId = new Guid(cookie.AsSpan(16, 16));

            // First check to see if there is a promoted LTM transaction with the same ID.  If there
            // is, just return that.
            transaction = TransactionManager.FindPromotedTransaction(txId);
            if (transaction != null)
            {
                if (etwLog.IsEnabled())
                {
                    etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromExportCookie");
                }

                return transaction;
            }

            // We need to create a new transaction
            RealOletxTransaction? realTx = null;
            OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager;

            oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1);
            try
            {
                outcomeEnlistment = new OutcomeEnlistment();
                oletxTm.DtcTransactionManager.ProxyShimFactory.Import(cookie, outcomeEnlistment, out txIdentifier, out oletxIsoLevel, out transactionShim);
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);

                // We are unsure of what the exception may mean.  It is possible that
                // we could get E_FAIL when trying to contact a transaction manager that is
                // being blocked by a fire wall.  On the other hand we may get a COMException
                // based on bad data.  The more common situation is that the data is fine
                // (since it is generated by Microsoft code) and the problem is with
                // communication.  So in this case we default for unknown exceptions to
                // assume that the problem is with communication.
                throw TransactionManagerCommunicationException.Create(SR.TraceSourceOletx, comException);
            }
            finally
            {
                oletxTm.DtcTransactionManagerLock.ReleaseReaderLock();
            }

            // We need to create a new RealOletxTransaction.
            realTx = new RealOletxTransaction(
                oletxTm,
                transactionShim,
                outcomeEnlistment,
                txIdentifier,
                oletxIsoLevel);

            // Now create the associated OletxTransaction.
            oleTx = new OletxTransaction(realTx);

            // If a transaction is found then FindOrCreate will Dispose the oletx
            // created.
            transaction = TransactionManager.FindOrCreatePromotedTransaction(txId, oleTx);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromExportCookie)}");
            }

            return transaction;
        }

        public static byte[] GetTransmitterPropagationToken(Transaction transaction)
        {
            ArgumentNullException.ThrowIfNull(transaction);

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransmitterPropagationToken)}");
            }

            // First, make sure we are working with an OletxTransaction.
            OletxTransaction oletxTx = ConvertToOletxTransaction(transaction);

            byte[] token = GetTransmitterPropagationToken(oletxTx);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransmitterPropagationToken)}");
            }

            return token;
        }

        internal static byte[] GetTransmitterPropagationToken(OletxTransaction oletxTx)
        {
            byte[]? propagationToken = null;

            try
            {
                propagationToken = oletxTx.RealOletxTransaction.TransactionShim.GetPropagationToken();
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);
                throw;
            }

            return propagationToken;
        }

        public static Transaction GetTransactionFromTransmitterPropagationToken(byte[] propagationToken)
        {
            ArgumentNullException.ThrowIfNull(propagationToken);

            if (propagationToken.Length < 24)
            {
                throw new ArgumentException(SR.InvalidArgument, nameof(propagationToken));
            }

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken");
            }

            // Extract the transaction guid from the propagation token to see if we already have a
            // transaction object for the transaction.
            // In a propagation token, the transaction guid is preceded by two version DWORDs.
            var txId = new Guid(propagationToken.AsSpan(8, 16));

            // First check to see if there is a promoted LTM transaction with the same ID.  If there is, just return that.
            Transaction? tx = TransactionManager.FindPromotedTransaction(txId);
            if (null != tx)
            {
                if (etwLog.IsEnabled())
                {
                    etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken");
                }

                return tx;
            }

            OletxTransaction dTx = GetOletxTransactionFromTransmitterPropagationToken(propagationToken);

            // If a transaction is found then FindOrCreate will Dispose the distributed transaction created.
            Transaction returnValue = TransactionManager.FindOrCreatePromotedTransaction(txId, dTx);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken");
            }
            return returnValue;
        }

        public static IDtcTransaction GetDtcTransaction(Transaction transaction)
        {
            ArgumentNullException.ThrowIfNull(transaction);

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetDtcTransaction)}");
            }

            IDtcTransaction? transactionComObject;

            // First, make sure we are working with an OletxTransaction.
            OletxTransaction oletxTx = ConvertToOletxTransaction(transaction);

            try
            {
                oletxTx.RealOletxTransaction.TransactionShim.GetITransactionNative(out ITransaction transactionNative);

                ComWrappers.TryGetComInstance(transactionNative, out IntPtr transactionNativePtr);

                transactionComObject = (IDtcTransaction)Marshal.GetObjectForIUnknown(transactionNativePtr);
                Marshal.SetComObjectData(transactionComObject, typeof(ITransaction), transactionNative);

                Marshal.Release(transactionNativePtr);
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);
                throw;
            }

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetDtcTransaction)}");
            }

            return transactionComObject;
        }

        internal static IDtcTransaction GetDtcTransaction(ITransaction transaction)
        {
            ComWrappers.TryGetComInstance(transaction, out IntPtr transactionNativePtr);

            var transactionNative = (IDtcTransaction)Marshal.GetObjectForIUnknown(transactionNativePtr);
            Marshal.SetComObjectData(transactionNative, typeof(ITransaction), transaction);

            Marshal.Release(transactionNativePtr);
            return transactionNative;
        }

        public static Transaction GetTransactionFromDtcTransaction(IDtcTransaction transactionNative)
        {
            ArgumentNullException.ThrowIfNull(transactionNative);

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromDtcTransaction)}");
            }

            Transaction? transaction = null;
            bool tooLate = false;
            TransactionShim? transactionShim = null;
            Guid txIdentifier = Guid.Empty;
            OletxTransactionIsolationLevel oletxIsoLevel = OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE;
            OutcomeEnlistment? outcomeEnlistment = null;
            RealOletxTransaction? realTx = null;
            OletxTransaction? oleTx = null;

            ITransaction myTransactionNative = GetITransactionFromIDtcTransaction(transactionNative);

            // Let's get the guid of the transaction from the proxy to see if we already have an object.
            OletxXactTransInfo xactInfo;
            try
            {
                myTransactionNative.GetTransactionInfo(out xactInfo);
            }
            catch (COMException ex) when (ex.ErrorCode == OletxHelper.XACT_E_NOTRANSACTION)
            {
                // If we get here, the transaction has appraently already been committed or aborted.  Allow creation of the
                // OletxTransaction, but it will be marked with a status of InDoubt and attempts to get its Identifier
                // property will result in a TransactionException.
                tooLate = true;
                xactInfo.Uow = Guid.Empty;
            }

            OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager;
            if (!tooLate)
            {
                // First check to see if there is a promoted LTM transaction with the same ID.  If there
                // is, just return that.
                transaction = TransactionManager.FindPromotedTransaction(xactInfo.Uow);
                if (transaction != null)
                {
                    if (etwLog.IsEnabled())
                    {
                        etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromDtcTransaction)}");
                    }

                    return transaction;
                }

                // We need to create a new RealOletxTransaction...
                oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1);
                try
                {
                    outcomeEnlistment = new OutcomeEnlistment();
                    oletxTm.DtcTransactionManager.ProxyShimFactory.CreateTransactionShim(
                        transactionNative,
                        outcomeEnlistment,
                        out txIdentifier,
                        out oletxIsoLevel,
                        out transactionShim);
                }
                catch (COMException comException)
                {
                    OletxTransactionManager.ProxyException(comException);
                    throw;
                }
                finally
                {
                    oletxTm.DtcTransactionManagerLock.ReleaseReaderLock();
                }

                // We need to create a new RealOletxTransaction.
                realTx = new RealOletxTransaction(
                    oletxTm,
                    transactionShim,
                    outcomeEnlistment,
                    txIdentifier,
                    oletxIsoLevel);

                oleTx = new OletxTransaction(realTx);

                // If a transaction is found then FindOrCreate will Dispose the oletx
                // created.
                transaction = TransactionManager.FindOrCreatePromotedTransaction(xactInfo.Uow, oleTx);
            }
            else
            {
                // It was too late to do a clone of the provided ITransactionNative, so we are just going to
                // create a RealOletxTransaction without a transaction shim or outcome enlistment.
                realTx = new RealOletxTransaction(
                    oletxTm,
                    null,
                    null,
                    txIdentifier,
                    OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE);

                oleTx = new OletxTransaction(realTx);
                transaction = new Transaction(oleTx);
                TransactionManager.FireDistributedTransactionStarted(transaction);
                oleTx.SavedLtmPromotedTransaction = transaction;

                InternalTransaction.DistributedTransactionOutcome(transaction._internalTransaction, TransactionStatus.InDoubt);
            }

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromDtcTransaction)}");
            }

            return transaction;
        }

        internal static ITransaction GetITransactionFromIDtcTransaction(IDtcTransaction transactionNative)
        {
            if (Marshal.GetComObjectData(transactionNative, typeof(ITransaction)) is not ITransaction myTransactionNative)
            {
                unsafe
                {
                    nint unknown = Marshal.GetIUnknownForObject(transactionNative);
                    if (Marshal.QueryInterface(unknown, Guids.IID_ITransaction_Guid, out IntPtr transactionNativePtr) == 0)
                    {
                        Marshal.Release(unknown);
                        myTransactionNative = ComInterfaceMarshaller<ITransaction>.ConvertToManaged((void*)transactionNativePtr)!;
                        Marshal.SetComObjectData(transactionNative, typeof(ITransaction), myTransactionNative);
                        Marshal.Release(transactionNativePtr);
                    }
                    else
                    {
                        Marshal.Release(unknown);
                        throw new ArgumentException(SR.InvalidArgument, nameof(transactionNative));
                    }
                }
            }

            return myTransactionNative;
        }

        public static byte[] GetWhereabouts()
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.${nameof(GetWhereabouts)}");
            }

            OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager;
            if (oletxTm == null)
            {
                throw new ArgumentException(SR.InvalidArgument, "transactionManager");
            }

            byte[]? returnValue;

            oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1);
            try
            {
                returnValue = oletxTm.DtcTransactionManager.Whereabouts;
            }
            finally
            {
                oletxTm.DtcTransactionManagerLock.ReleaseReaderLock();
            }

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.${nameof(GetWhereabouts)}");
            }

            return returnValue;
        }

        internal static OletxTransaction GetOletxTransactionFromTransmitterPropagationToken(byte[] propagationToken)
        {
            ArgumentNullException.ThrowIfNull(propagationToken);

            if (propagationToken.Length < 24)
            {
                throw new ArgumentException(SR.InvalidArgument, nameof(propagationToken));
            }

            Guid identifier;
            OletxTransactionIsolationLevel oletxIsoLevel;
            OutcomeEnlistment outcomeEnlistment;
            TransactionShim? transactionShim = null;

            byte[] propagationTokenCopy = new byte[propagationToken.Length];
            Array.Copy(propagationToken, propagationTokenCopy, propagationToken.Length);
            propagationToken = propagationTokenCopy;

            // First we need to create an OletxTransactionManager from Config.
            OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager;

            oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1);
            try
            {
                outcomeEnlistment = new OutcomeEnlistment();
                oletxTm.DtcTransactionManager.ProxyShimFactory.ReceiveTransaction(
                    propagationToken,
                    outcomeEnlistment,
                    out identifier,
                    out oletxIsoLevel,
                    out transactionShim);
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);

                // We are unsure of what the exception may mean.  It is possible that
                // we could get E_FAIL when trying to contact a transaction manager that is
                // being blocked by a fire wall.  On the other hand we may get a COMException
                // based on bad data.  The more common situation is that the data is fine
                // (since it is generated by Microsoft code) and the problem is with
                // communication.  So in this case we default for unknown exceptions to
                // assume that the problem is with communication.
                throw TransactionManagerCommunicationException.Create(SR.TraceSourceOletx, comException);
            }
            finally
            {
                oletxTm.DtcTransactionManagerLock.ReleaseReaderLock();
            }

            var realTx = new RealOletxTransaction(
                oletxTm,
                transactionShim,
                outcomeEnlistment,
                identifier,
                oletxIsoLevel);

            return new OletxTransaction(realTx);
        }
    }
}
